<!DOCTYPE HTML>
<html>
<head>
  <title>Test tail time lifetime of PannerNode</title>
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="text/javascript" src="webaudio.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<pre id="test">
<script class="testbody" type="text/javascript">

// This tests that a PannerNode does not release its reference before
// it finishes emitting sound.
//
// The PannerNode tail time is short, so, when a PannerNode is destroyed on
// the main thread, it is unlikely to notify the graph thread before the tail
// time expires.  However, by adding DelayNodes downstream from the
// PannerNodes, the graph thread can have enough time to notice that a
// DelayNode has been destroyed.
//
// In the current implementation, DelayNodes will take a tail-time reference
// immediately when they receive the first block of sound from an upstream
// node, so this test connects the downstream DelayNodes while the upstream
// nodes are finishing, and then runs GC (on the main thread) before the
// DelayNodes receive any input (on the graph thread).
//
// Web Audio doesn't provide a means to precisely time connect()s but we can
// test that the output of delay nodes matches the output from a reference
// PannerNode that we know will not be GCed.
//
// Another set of delay nodes is added upstream to ensure that the source node
// has removed its self-reference after dispatching its "ended" event.

SimpleTest.waitForExplicitFinish();

const blockSize = 128;
// bufferSize should be long enough that to allow an audioprocess event to be
// sent to the main thread and a connect message to return to the graph
// thread.
const bufferSize = 4096;
const pannerCount = bufferSize / blockSize;
// sourceDelayBufferCount should be long enough to allow the source node
// onended to finish and remove the source self-reference.
const sourceDelayBufferCount = 3;
var gotEnded = false;
// ccDelayLength should be long enough to allow CC to run
var ccDelayBufferCount = 20;
const ccDelayLength = ccDelayBufferCount * bufferSize;

var ctx;
var testPanners = [];
var referencePanner;
var referenceProcessCount = 0;
var referenceOutput = [new Float32Array(bufferSize),
                       new Float32Array(bufferSize)];
var testProcessor;
var testProcessCount = 0;

function isChannelSilent(channel) {
  for (var i = 0; i < channel.length; ++i) {
    if (channel[i] != 0.0) {
      return false;
    }
  }
  return true;
}

function onReferenceOutput(e) {
  switch(referenceProcessCount) {

  case sourceDelayBufferCount - 1:
    // The panners are about to finish.
    if (!gotEnded) {
      todo(false, "Source hasn't ended.  Increase sourceDelayBufferCount?");
    }

    // Connect each PannerNode output to a downstream DelayNode,
    // and connect ScriptProcessors to compare test and reference panners.
    var delayDuration = ccDelayLength / ctx.sampleRate;
    for (var i = 0; i < pannerCount; ++i) {
      var delay = ctx.createDelay(delayDuration);
      delay.delayTime.value = delayDuration;
      delay.connect(testProcessor);
      testPanners[i].connect(delay);
    }
    testProcessor = null;
    testPanners = null;

    // The panning effect is linear so only one reference panner is required.
    // This also checks that the individual panners don't chop their output
    // too soon.
    referencePanner.connect(e.target);

    // Assuming the above operations have already scheduled an event to run in
    // stable state and ask the graph thread to make connections, schedule a
    // subsequent event to run cycle collection, which should not collect
    // panners that are still producing sound.
    SimpleTest.executeSoon(function() {
      SpecialPowers.forceGC();
      SpecialPowers.forceCC();
    });

    break;

  case sourceDelayBufferCount:
    // Record this buffer during which PannerNode outputs were connected.
    for (var i = 0; i < 2; ++i) {
      e.inputBuffer.copyFromChannel(referenceOutput[i], i);
    }
    e.target.onaudioprocess = null;
    e.target.disconnect();

    // If the buffer is silent, there is probably not much point just
    // increasing the buffer size, because, with the buffer size already
    // significantly larger than panner tail time, it demonstrates that the
    // lag between threads is much greater than the tail time.
    if (isChannelSilent(referenceOutput[0])) {
      todo(false, "Connections not detected.");
    }
  }

  referenceProcessCount++;
}

function onTestOutput(e) {
  if (testProcessCount < sourceDelayBufferCount + ccDelayBufferCount) {
    testProcessCount++;
    return;
  }

  for (var i = 0; i < 2; ++i) {
    compareChannels(e.inputBuffer.getChannelData(i), referenceOutput[i]);
  }
  e.target.onaudioprocess = null;
  e.target.disconnect();
  SimpleTest.finish();
}

function startTest() {
  // 0.002 is MaxDelayTimeSeconds in HRTFpanner.cpp
  // and 512 is fftSize() at 48 kHz.
  const expectedPannerTailTime = 0.002 * ctx.sampleRate + 512;

  // Create some PannerNodes downstream from DelayNodes with delays long
  // enough for their source to finish, dispatch its "ended" event
  // and release its playing reference.  The DelayNodes should expire their
  // tail-time references before the PannerNodes and so only the PannerNode
  // lifetimes depends on their tail-time references.  Many DelayNodes are
  // created and timed to finish at different times so that one PannerNode
  // will be finishing the block processed immediately after the connect is
  // received.
  var source = ctx.createBufferSource();
  // Just short of blockSize here to avoid rounding into the next block
  var buffer = ctx.createBuffer(1, blockSize - 1, ctx.sampleRate);
  for (var i = 0; i < buffer.length; ++i) {
    buffer.getChannelData(0)[i] = Math.cos(Math.PI * i / buffer.length);
  }
  source.buffer = buffer;
  source.start(0);
  source.onended = function(e) {
    gotEnded = true;
  };

  // Time the first test panner to finish just before downstream DelayNodes
  // are about the be connected.  Note that DelayNode lifetime depends on
  // maxDelayTime so set that equal to the delay.
  var delayDuration =
    (sourceDelayBufferCount * bufferSize
     - expectedPannerTailTime - 2 * blockSize) / ctx.sampleRate;

  for (var i = 0; i < pannerCount; ++i) {
    var delay = ctx.createDelay(delayDuration);
    delay.delayTime.value = delayDuration;
    source.connect(delay);
    delay.connect(referencePanner)

    var panner = ctx.createPanner();
    panner.type = "HRTF";
    delay.connect(panner);
    testPanners[i] = panner;

    delayDuration += blockSize / ctx.sampleRate;
  }

  // Create a ScriptProcessor now to use as a timer to trigger connection of
  // downstream nodes.  It will also be used to record reference output.
  var referenceProcessor = ctx.createScriptProcessor(bufferSize, 2, 0);
  referenceProcessor.onaudioprocess = onReferenceOutput;
  // Start audioprocess events before source delays are connected.
  referenceProcessor.connect(ctx.destination);

  // The test ScriptProcessor will record output of testPanners. 
  // Create it now so that it is synchronized with the referenceProcessor.
  testProcessor = ctx.createScriptProcessor(bufferSize, 2, 0);
  testProcessor.onaudioprocess = onTestOutput;
  // Start audioprocess events before source delays are connected.
  testProcessor.connect(ctx.destination);
}

function prepareTest() {
  ctx = new AudioContext();
  // Place the listener to the side of the origin, where the panners are
  // positioned, to maximize delay in one ear.
  ctx.listener.setPosition(1,0,0);

  // A PannerNode will produce no output until it has loaded its HRIR
  // database.  Wait for this to load before starting the test.
  var processor = ctx.createScriptProcessor(bufferSize, 2, 0);
  referencePanner = ctx.createPanner();
  referencePanner.type = "HRTF";
  referencePanner.connect(processor);
  var oscillator = ctx.createOscillator();
  oscillator.connect(referencePanner);
  oscillator.start(0);

  processor.onaudioprocess = function(e) {
    if (isChannelSilent(e.inputBuffer.getChannelData(0)))
      return;

    oscillator.stop(0);
    oscillator.disconnect();
    referencePanner.disconnect();
    e.target.onaudioprocess = null;
    SimpleTest.executeSoon(startTest);
  };
}
prepareTest();
</script>
</pre>
</body>
</html>
